package com.fourtwenty.hemptress.plugin;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fourtwenty.hemptress.PluginEventBot;
import com.fourtwenty.hemptress.plugin.annotate.Plugin;
import com.fourtwenty.hemptress.plugin.annotate.Property;
import com.fourtwenty.hemptress.plugin.annotate.RequiresPlugin;

public class PluginLoader {
	private static final Logger LOG = LoggerFactory.getLogger(PluginLoader.class);
	private static final Comparator<PluginDescriptor> NAME_COMPARE =  new Comparator<PluginDescriptor>() {
		@Override
		public int compare(PluginDescriptor o1, PluginDescriptor o2) {
			return o1.getName().equalsIgnoreCase(o2.getName()) ? 0 : -1;
		}
	};
	
	private File directory;
	private Map<String, PluginDescriptor> plugins = new HashMap<>();
	private DependencyTree<PluginDescriptor> dependencyTree = new DependencyTree<PluginDescriptor>("core-null", null);
	private PluginEventSource events = new PluginEventSource();
	
	public PluginLoader(File directory) {
		this.directory = directory;
	}

	public void loadPlugins(PluginEventBot bot) throws PluginLoadingException {
		System.out.println("Scanning for plugins in directory: " + directory.getAbsolutePath());
		try {
			//step 1: scan directory and create PluginDescriptors for all of them
			String[] jars = directory.list(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {
					return name.endsWith(".jar");
				}
			});
			
			
			for(String jar : jars) {
				String name = jar.substring(0, jar.length()-4);
				loadPlugin(name, null);
			}
			
			//step 2: use the dep tree to initialize plugins in the correct order
			for(DependencyTree<PluginDescriptor> pluginTree : dependencyTree.flatten()) {
				PluginDescriptor plugin = pluginTree.getElement();
				plugin = plugins.get(plugin.getName()); 
				
				if(plugin.isReady()) {
					continue;
				}
				
				Properties props = new Properties();
				File propertyFile = new File(directory, plugin.getName() + ".properties");
				if(LOG.isDebugEnabled()) {
					LOG.debug(propertyFile.exists() + " " + propertyFile.getAbsoluteFile());
				}
				if(propertyFile.exists()) {
					try {
						props.load(new FileInputStream(propertyFile));
					} catch (IOException e) {
						LOG.error("Could not load properties", e);
					}
				}

				
				for(Class<? extends Plugin> clazz : plugin.getClasses()) {
					if(!plugin.getInstances().containsKey(clazz.getCanonicalName())) {
						Object instance;
						try {
							try {
								Constructor<? extends Plugin> constructor = clazz.getConstructor(PluginEventBot.class);
								instance = constructor.newInstance(bot);
							} catch(NoSuchMethodException | IllegalArgumentException | InvocationTargetException e) {
								instance = clazz.newInstance();
							}
							
							
							
							plugin.getInstances().put(clazz.getCanonicalName(), instance);
							
							for(Method method : instance.getClass().getMethods()) {
								events.registerMethod(method, instance, plugin.getName());
							
								if(method.getName().startsWith("set")) {
									Class<?>[] params = method.getParameterTypes();
									
									if(params.length != 1 || !params[0].equals(String.class)) {
										continue;
									}
									
									Property prop = method.getAnnotation(Property.class);
									if(prop != null) {
										try {
											Object propVal = props.getProperty(prop.value(), "");
											method.invoke(instance, propVal);
										} catch (IllegalArgumentException | InvocationTargetException e) {
											e.printStackTrace();
											LOG.error("Could not call plugin setter", e);
										}
									}
								}
							}
							
						} catch (InstantiationException | IllegalAccessException e) {
							LOG.error("Could not instantiate plugin class", e);
							e.printStackTrace();
						}
					}
				}
				
				plugin.setReady(true);
			}
			
			//step 3: register plugin instances with where they need to be registered..
			
		} catch (PluginLoadingException e) {
			LOG.error("Trouble loading plugins", e);
			throw e;
		}
		
	}
	
	@SuppressWarnings("unchecked")
	public void loadPlugin(final String pluginName, PluginDescriptor parent) throws PluginLoadingException {
		if(plugins.containsKey(pluginName)) {
			PluginDescriptor oldPlugin = plugins.get(pluginName);
			
			if(parent != null) {
				try {
					DependencyTree<PluginDescriptor> depTree = dependencyTree.find(oldPlugin);
					dependencyTree.find(parent, NAME_COMPARE).addDependency(depTree);
				} catch (CircularDependencyException e) {
					LOG.error("Exception loading plugin.", e);
					throw new PluginLoadingException(e);
				} 
			}  
		}
	
		//find the plugin
		String[] jars = directory.list(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return name.endsWith(".jar") && name.equalsIgnoreCase(pluginName + ".jar");
			}
		});
		
		if(jars.length < 1) {
			throw new PluginLoadingException("Could not find plugin named " + pluginName);
		}

		File[] files = directory.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return name.endsWith(".jar");
			}
		}); 
				
		URL[] urls = new URL[files.length];
		for (int i = 0; i < files.length; i++) {
			File f = files[i];
			try {
				urls[i] = f.toURI().toURL();
			} catch (MalformedURLException e) {
			}
		}
		
		ClassLoader parentLoader = this.getClass().getClassLoader(); 
		
		try (
			URLClassLoader loader =
					new URLClassLoader(urls, parentLoader);
				
			JarFile jar = new JarFile(new File(directory, jars[0]));
		){
			PluginDescriptor newPlugin = new PluginDescriptor();
			newPlugin.setName(pluginName);
		
			if(parent == null) {
				dependencyTree.addDependency(new DependencyTree<PluginDescriptor>(pluginName, newPlugin));
			} else {
				dependencyTree.find(parent, NAME_COMPARE).addDependency(new DependencyTree<PluginDescriptor>(pluginName, newPlugin));
			}
			
			Enumeration<JarEntry> jarEnum = jar.entries();
			
			while(jarEnum.hasMoreElements()) {
				JarEntry entry = jarEnum.nextElement();
				
				if(entry.isDirectory()) {
					continue;
				}
				
				//scan for classes
				if(entry.getName().endsWith(".class") ){ 
					String className = entry.getName().substring(0, entry.getName().length() - 6).replaceAll("/|\\\\", ".");
					try {
						Class<?> clazz = loader.loadClass(className);
						
						if(clazz.getAnnotation(Plugin.class) != null) {
							//add to plugin descriptor
							newPlugin.getClasses().add((Class<? extends Plugin>) clazz);
							
							//check for dependencies...
							if(clazz.getAnnotation(RequiresPlugin.class) != null) {
								RequiresPlugin rp = clazz.getAnnotation(RequiresPlugin.class);
								String[] dependencies = rp.value();
								
								newPlugin.getDependencies().addAll(Arrays.asList(dependencies));
							}
						}
					} catch (ClassNotFoundException e) {
						throw new PluginLoadingException(e);
					}
				}
			}
			
			plugins.put(pluginName, newPlugin);
			
			
			for(String dep : newPlugin.getDependencies()) {
				loadPlugin(dep, newPlugin);
			}
		} catch (IOException | CircularDependencyException e) {
			LOG.error("Exception loading plugin.", e);
			throw new PluginLoadingException(e);
		}		
	}

	public PluginEventSource getEvents() {
		return events;
	}

	public void setEvents(PluginEventSource events) {
		this.events = events;
	}

	public PluginStatus getPluginStatus(String plugin) {
		return events.getPluginStatus(plugin);
	}

	public void enablePlugin(String plugin) {
		events.enablePlugin(plugin);
	}

	public void disablePlugin(String plugin) {
		events.disablePlugin(plugin);
	}

	public Set<String> getEnabledPlugins() {
		return events.getEnabledPlugins();
	}

	public Set<String> getDisabledPlugins() {
		return events.getDisabledPlugins();
	}
	
}
